package model;

import java.awt.Color;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ExecutionException;

import javax.swing.SwingWorker;

import model.BD.GeolocalizacionesDAO;
import model.BD.ViajesDAO;
import model.Constants.EstadosSimulacion;
import model.Constants.EstadosViaje;
import model.Constants.ValoresSimulacion;
import view.SetulatorVista;

import com.javadocmd.simplelatlng.LatLng;
import com.javadocmd.simplelatlng.LatLngTool;
import com.javadocmd.simplelatlng.util.LengthUnit;

/**
 * Clase utilizada para simular el viaje.
 * 
 */
public class Simulador extends SwingWorker<String, String> {

	private SetulatorVista setulatorVista;

	private int velocidadMH = 40000;
	private List<LatLng> ruta;
	private Viaje viaje;
	private double distTotal = 0;
	private double distActual = 0;
	private int paradaActual = 0;
	private boolean paradaActualGrabada;
	private int tramoActual = 0;
	private boolean traficoPesado = false;
	private int estadoVehiculo = ValoresSimulacion.ESTADO_MOVIMIENTO;
	private boolean stopThread = false;
	private boolean ultimaParadaVisitada = false;
	private int proximaParada;
	private Map<Integer, Integer> indexParadasPorId;
	private int metrosRestantes = 0;
	private int segundosRestantes = 0;

	/**
	 * Constructor del simulador.
	 * 
	 * @param setulatorVista
	 */
	public Simulador(SetulatorVista setulatorVista) {
		this.setulatorVista = setulatorVista;
	}

	/**
	 * Dispara el hilo del simulador una vez presionado Iniciar Simulacion.
	 */
	@Override
	public String doInBackground() {
		ViajesDAO viajesDAO = ViajesDAO.obtenerInstancia();
		Ruta rutaViaje = this.viaje.getRuta();
		indexParadasPorId = new HashMap<Integer, Integer>();
		ruta = obtenerRutaDelTrazado(rutaViaje.getTrazado());
		List<InformacionTramo> infoTramos = obtenerInfoDeLosTramos(ruta);
		List<PuntoParada> puntosRuta = actualizarValoresEnPuntos(this.viaje.getRuta().getParadasPorRuta(), infoTramos);
		viajesDAO.actualizarEstado(this.viaje.getIdViaje(), EstadosViaje.VIAJE_EN_CURSO);
		return simular(this.viaje, infoTramos, puntosRuta);
	}

	/**
	 * Se ejecuta al terminar el hilo de la simulacion y actualiza el estado del
	 * simulador.
	 */
	@Override
	public void done() {
		try {
			String resultado = get();
			System.out.println("::Resultado:: " + resultado);
			this.setulatorVista.btnDetenerSimulacion.setEnabled(false);
			this.setulatorVista.viajesBox.setEnabled(true);
			this.setulatorVista.listaParadas.removeAll();
			if(EstadosSimulacion.SIMULACION_DETENIDA.equalsIgnoreCase(resultado)) {
				this.setulatorVista.mensaje.setForeground(Color.red);
			} else {
				this.setulatorVista.mensaje.setForeground(Color.green);
			}
			this.setulatorVista.mensaje.setText(String.format("%s para el Viaje %d", resultado, this.viaje.getIdViaje()));
			this.setulatorVista.simulador = null;
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				System.out.println("Simulador.done(): Error pausando hilo");
			}
			this.setulatorVista.viajesBox.setModel(ComboViajesHandler.crearComboViajes());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {

			e.printStackTrace();
		}
	}

	/**
	 * Flujo principal del simulador.
	 * 
	 * @param viajeSimulado
	 * @param infoTramos
	 * @param puntosParada
	 * @return String EstadosSimulacion
	 */
	private String simular(Viaje viajeSimulado, List<InformacionTramo> infoTramos, List<PuntoParada> puntosParada) {

		long tiempoInicial = System.currentTimeMillis();
		long tiempoCalculoInicial = System.currentTimeMillis();
		int indexUltimoElemento = infoTramos.size() - 1;
		long tiempoDeDetencion = 0;
		this.paradaActualGrabada = true;
		distTotal = infoTramos.get(indexUltimoElemento).getDistanciaAcumulada();
		System.out.println("Simulador.simular(): Distancia total del viaje: " + distTotal);
		this.distActual = 0;
		GeolocalizacionesDAO geolocalizacionesDAO = GeolocalizacionesDAO.obtenerInstancia();
		ViajesDAO viajesDAO = ViajesDAO.obtenerInstancia();
		int idParadaInicial = puntosParada.get(0).getIdPunto();
		long timestampActual = 0;
		LatLng posDest = new LatLng(0,0);
			
		// Mientras no haya completado la ruta, no se detenga desde el
		// simulador, no haya llegado a la ultima parada.
		while ((this.distActual <= distTotal) && !stopThread && !ultimaParadaVisitada) {

			long tiempoActual = System.currentTimeMillis();
			long difTiempoUpdate = (tiempoActual - tiempoInicial) / 1000;
			long difTiempoCalculo = (tiempoActual - tiempoCalculoInicial) / 1000;
			long arriboProximaParada = 0;

			if (difTiempoCalculo >= ValoresSimulacion.T_CALCULO_SEG) {

				tiempoCalculoInicial = System.currentTimeMillis();

				if (arriboParada(puntosParada, infoTramos)) {
					calcularVelocidad();
					tiempoDeDetencion = System.currentTimeMillis();
				} else if (tiempoDeDetencionAgotado(tiempoDeDetencion)) {
					estadoVehiculo = ValoresSimulacion.ESTADO_MOVIMIENTO;
					calcularVelocidad();
				}

				// Calcula la distancia recorrida hasta el momento
				long distRecorrida = difTiempoCalculo * velocidadMH / ValoresSimulacion.SEG_HORA;
				this.distActual += distRecorrida;

				// Evalua si se termino la ruta
				if (this.distActual >= distTotal) {
					break;
				}

				// update base cada 2 segundos, si es la ultima parada hace el
				// update sin importar el timepo de refresco
				if (difTiempoUpdate >= ValoresSimulacion.T_REFRESCO_SEG || ultimaParadaVisitada) {

					tiempoInicial = System.currentTimeMillis();
					posDest = calcularPosicion(this.distActual, infoTramos);
					arriboProximaParada = calcularArribo(this.distActual, viajeSimulado, proximaParada);
					timestampActual = System.currentTimeMillis();

					// Si la aprada actual todavia no fua grabada, graba el id
					// del punto en la base
					if (!this.paradaActualGrabada) {

						// Si es el punto inicial graba el timestamp del horario
						// real de salida en la base
						if (idParadaInicial == paradaActual) {

							System.out.println("Simulador.simular(): Grabar timestamp real salida: " + timestampActual);
							viajesDAO.actualizarTimestampSalida(viajeSimulado.getIdViaje(), timestampActual);

							// Si es el punto final graba el timestamp del
							// horario real de llegada en la base
						} else if (ultimaParadaVisitada) {

							break;
							//viajesDAO.actualizarTimestampLlegada(viajeSimulado.getIdViaje(), timestampActual);

						}
						geolocalizacionesDAO.insertarCoordenadas(viajeSimulado, posDest.getLatitude(), posDest.getLongitude(), velocidadMH, paradaActual, proximaParada, arriboProximaParada, timestampActual);
						this.paradaActualGrabada = true;

					} else {

						geolocalizacionesDAO.insertarCoordenadas(viajeSimulado, posDest.getLatitude(), posDest.getLongitude(), velocidadMH, Types.NULL, proximaParada, arriboProximaParada, timestampActual);

					}

				}
			}

		}
		this.tramoActual = 0;
		if (stopThread) {
			// Simulacion detenida por el usuario
			viajesDAO.actualizarEstado(viajeSimulado.getIdViaje(), EstadosViaje.VIAJE_BORRADO);
			return EstadosSimulacion.SIMULACION_DETENIDA;
		} else {
			// Viaje finalizado
			System.out.println("Simulador.simular(): Grabar timestamp real llegada: " + timestampActual);
			//Inserta las coordenadas de la parada final
			if(puntosParada != null && !puntosParada.isEmpty()) {
				int ultimaParadaId = puntosParada.get(puntosParada.size()-1).getIdPunto();
				geolocalizacionesDAO.insertarCoordenadas(viajeSimulado, posDest.getLatitude(), posDest.getLongitude(), velocidadMH, ultimaParadaId, ultimaParadaId, System.currentTimeMillis(), timestampActual);
			}
			viajesDAO.actualizarTimestampLlegada(viajeSimulado.getIdViaje(), timestampActual);
			viajesDAO.actualizarEstado(viajeSimulado.getIdViaje(), EstadosViaje.VIAJE_FINALIZADO);
		}
		return EstadosSimulacion.SIMULACION_FINALIZADA;
	}

	/**
	 * Calcula el tiempo de arribo a la proxima parada.
	 * 
	 * @param distanciaActual
	 * @param viajeSimulado
	 * @param iDproximaParada
	 * @return long arribo
	 */
	private long calcularArribo(double distanciaActual, Viaje viajeSimulado, int iDproximaParada) {

		List<PuntoParada> paradas = viajeSimulado.getRuta().getParadasPorRuta();
		long arribo = 0;

		for (PuntoParada parada : paradas) {
			if (iDproximaParada == parada.getIdPunto()) {

				double deltaDistancia = parada.getDistanciaEnRuta() < distanciaActual ? 0 : (parada.getDistanciaEnRuta() - distanciaActual);
				metrosRestantes = (int) deltaDistancia;
				segundosRestantes = (int) ((parada.getTiempoEsperadoArribo() - System.currentTimeMillis()) / 1000);
				long tiempoLlegadaCalculado;

				if (estadoVehiculo == ValoresSimulacion.ESTADO_MOVIMIENTO) {
					
					if (deltaDistancia <= 0) {
						tiempoLlegadaCalculado = System.currentTimeMillis() + 1000;
					} else {
						tiempoLlegadaCalculado = (long) (System.currentTimeMillis() + ((deltaDistancia * ValoresSimulacion.SEG_HORA / velocidadMH) * 1000));
					}

					// promedio entre estimacion y calculo
					if (parada.getTiempoEsperadoArribo() > System.currentTimeMillis()) {
						arribo = (tiempoLlegadaCalculado + parada.getTiempoEsperadoArribo()) / 2;
					} else {
						arribo = tiempoLlegadaCalculado;
					}

					System.out.println("Simulador.calcularArribo(): VelocidadMH: " + velocidadMH);
					System.out.println("Simulador.calcularArribo(): Tiempo arribo: " + ((arribo - System.currentTimeMillis()) / 1000));

				} else {

					arribo = parada.getTiempoEsperadoArribo() <= System.currentTimeMillis() ? System.currentTimeMillis() + 5
							: parada.getTiempoEsperadoArribo();

				}
				return arribo;
			}
		}
		return 0;
	}

	/**
	 * Evalua si el tiempo de detencion en la parada se agoto.
	 * 
	 * @param tiempoDeDetencion
	 * @return boolean true si se agoto, false si no se agoto
	 */
	private boolean tiempoDeDetencionAgotado(long tiempoDeDetencion) {
		return ((System.currentTimeMillis() - tiempoDeDetencion) / 1000) >= ValoresSimulacion.TIEMPO_DETENCION;
	}

	/**
	 * Calcula la velocidad del vehiculo basandose en la distancia que falta y
	 * el tiempo estimado para completarla.
	 */
	private void calcularVelocidad() {
		if (estadoVehiculo == ValoresSimulacion.ESTADO_DETENIDO) {
			velocidadMH = 0;
		} else {
			double probTraficoNormal = Math.random();

			if (segundosRestantes > 0) {

				velocidadMH = ((metrosRestantes * ValoresSimulacion.SEG_HORA) / segundosRestantes);
				velocidadMH += factorAleatorio(velocidadMH, traficoPesado);

			} else {

				if (probTraficoNormal <= 0.5) {
					velocidadMH = 30000 + (int) (Math.random() * ((35000 - 30000) + 1));
				} else {
					velocidadMH = 10000 + (int) (Math.random() * ((15000 - 10000) + 1));
				}

			}
		}

	}

	/**
	 * Varia la velocidad en base a un factor aleatorio.
	 * 
	 * @param velocidadActual
	 * @param traficoPesado
	 * @return int factorAleatorio
	 */
	private int factorAleatorio(int velocidadActual, boolean traficoPesado) {

		Random generador = new Random();
		int variacionGenerada = generador.nextInt(ValoresSimulacion.RANGO_VARIACION_ALEATORIA)
				- (traficoPesado ? ValoresSimulacion.RANGO_VARIACION_TRAFICO_PESADO : ValoresSimulacion.RANGO_VARIACION_TRAFICO_NORMAL);

		if ((velocidadActual + variacionGenerada) > 0) {

			return (variacionGenerada * 1000);

		} else {

			return 0;

		}
	}

	/**
	 * Arma los tramos de la ruta a partir de las coordenadas de la ruta.
	 * 
	 * @param ruta
	 * @return List<InformacionTramo> infoTramos
	 */
	private List<InformacionTramo> obtenerInfoDeLosTramos(List<LatLng> ruta) {

		List<InformacionTramo> infoTramos = new ArrayList<InformacionTramo>();
		for (int i = 0; i < ruta.size() - 1; i++) {
			LatLng puntoInicial = ruta.get(i);
			LatLng puntoFinal = ruta.get(i + 1);

			InformacionTramo infoTramo = new InformacionTramo(puntoInicial, puntoFinal);
			double longitudTRamo = LatLngTool.distance(puntoInicial, puntoFinal, LengthUnit.METER);
			infoTramo.setLongitudTramo(longitudTRamo);
			if (i > 0) {
				double distanciaAcumulada = infoTramos.get(i - 1).getDistanciaAcumulada() + longitudTRamo;
				infoTramo.setDistanciaAcumulada(distanciaAcumulada);
			} else {
				infoTramo.setDistanciaAcumulada(longitudTRamo);
			}

			infoTramos.add(infoTramo);
		}

		return infoTramos;
	}

	/**
	 * Obtiene el trazado de la ruta a partir de las coordenadas del poligono
	 * cargado en la base.
	 * 
	 * @param trazado
	 * @return List<LatLng> rutaDelTrazado
	 */
	private List<LatLng> obtenerRutaDelTrazado(List<PuntoRuta> trazado) {
		List<LatLng> rutaDelTrazado = new ArrayList<LatLng>();
		for (PuntoRuta punto : trazado) {
			LatLng p = new LatLng(punto.getLatitud(), punto.getLongitud());
			rutaDelTrazado.add(p);
		}
		return rutaDelTrazado;
	}

	/**
	 * Calcula la posicion actual del vehiculo en base a la distancia recorrida
	 * en la ruta.
	 * 
	 * @param distActual
	 * @param infoTramos
	 * @return LatLng posDest
	 */
	private LatLng calcularPosicion(double distActual, List<InformacionTramo> infoTramos) {

		for (int i = tramoActual; i < infoTramos.size(); i++) {

			InformacionTramo info = infoTramos.get(i);

			if (info.distanciaDentroDeTramo(distActual)) {

				double initialBearing = LatLngTool.initialBearing(info.getPuntoInicial(), info.getPuntoFinal());
				double distEnTramo = distActual - (info.getDistanciaAcumulada() - info.getLongitudTramo());
				LatLng posDest = LatLngTool.travel(info.getPuntoInicial(), initialBearing, distEnTramo, LengthUnit.METER);
				this.tramoActual = i;
				return posDest;

			}
		}
		return null;
	}

	/**
	 * Calcula si ya llego a la parada.
	 * 
	 * @param paradasRuta
	 * @param infoTramos
	 * @return boolean true si llego a la parada, false si no llego
	 */
	private boolean arriboParada(List<PuntoParada> paradasRuta, List<InformacionTramo> infoTramos) {

		if (estadoVehiculo == ValoresSimulacion.ESTADO_MOVIMIENTO) {
			LatLng posActual = calcularPosicion(distActual, infoTramos);
			for (PuntoParada paradaRuta : paradasRuta) {

				if (!paradaRuta.isPuntoVisitado()) {
					LatLng posPunto = new LatLng(paradaRuta.getLatitud(), paradaRuta.getLongitud());
					double distanciaParada = LatLngTool.distance(posActual, posPunto, LengthUnit.METER);
					if (distanciaParada < ValoresSimulacion.DISTANCIA_MINIMA_PARADA) {
						if (!paradaRuta.isSaltearPunto()) {
							System.out.println("Simulador.arriboParada(): PARADA: " + paradaRuta.getIdPunto() + " Distancia actual: " + distActual);
							this.paradaActualGrabada = false;
							if (paradaRuta.getIdPunto() == paradasRuta.get(paradasRuta.size() - 1).getIdPunto()) {
								ultimaParadaVisitada = true;
							}
							
							estadoVehiculo = ValoresSimulacion.ESTADO_DETENIDO;
							
						} else {
							System.out.println("Simulador.arriboParada(): PARADA SALTEADA: " + paradaRuta.getIdPunto() + " Distancia actual: " + distActual);
						}
						paradaRuta.setTiempoArribo(System.currentTimeMillis());
						
						long diferenciaEstimacion = System.currentTimeMillis() - paradaRuta.getTiempoEsperadoArribo();
						// ajusta la estimacion de todas las paradas con la
						// diferencia entre la llegada y lo estimado
						paradaRuta.setDiferenciaConEstimacion(diferenciaEstimacion);
						ajustarEstimacionEnParadas(paradasRuta, diferenciaEstimacion, paradaRuta.getIdPunto());
						
						paradaRuta.setPuntoVisitado(true);
						paradaActual = paradaRuta.getIdPunto();
						proximaParada = obtenerFkProximoPunto(paradasRuta, paradaActual);

						return true;
					}
				}
			}
		}

		return false;
	}

	/**
	 * Actualiza la estimacion en las paradas en base a la diferencia entre lo
	 * real y lo estimado.
	 * 
	 * @param paradasRuta
	 * @param diferenciaEstimacion
	 * @param idParada
	 */
	private void ajustarEstimacionEnParadas(List<PuntoParada> paradasRuta, long diferenciaEstimacion, int idParada) {
		int indexParada = indexParadasPorId.get(idParada);
		for (int i = indexParada; i < paradasRuta.size(); i++) {
			paradasRuta.get(i).setTiempoEsperadoArribo(paradasRuta.get(i).getTiempoEsperadoArribo() + diferenciaEstimacion);
		}

	}

	/**
	 * Obtiene el fk del proximo punto al pasado por parametro.
	 * 
	 * @param listaParadas
	 * @param paradaActual
	 * @return int fkProximoPunto
	 */
	private int obtenerFkProximoPunto(List<PuntoParada> listaParadas, int paradaActual) {

		int proximoIndex = 0;
		int indexActual = indexParadasPorId.get(paradaActual);
		int longitudListaPuntos = listaParadas.size();
		proximoIndex = indexActual + 1;
		if (proximoIndex < longitudListaPuntos) {
			return listaParadas.get(proximoIndex).getIdPunto();
		} else if (proximoIndex == longitudListaPuntos) {
			return paradaActual;
		}
		return 0;
	}

	/**
	 * Actualiza la distancia en ruta y los segundos desde la parada anterior en
	 * cada parada.
	 * 
	 * @param paradasPorRuta
	 * @param infoTramos
	 * @return List<PuntoParada> paradasPorRuta
	 */
	private List<PuntoParada> actualizarValoresEnPuntos(List<PuntoParada> paradasPorRuta, List<InformacionTramo> infoTramos) {
		long estimacionLlegada = System.currentTimeMillis();
		int i = 0;
		for (PuntoParada puntoParada : paradasPorRuta) {
			indexParadasPorId.put(puntoParada.getIdPunto(), i);

			for (InformacionTramo infoTramo : infoTramos) {
				if (infoTramo.puntoDentroDelTramo(puntoParada.getLatitud(), puntoParada.getLongitud())) {

					puntoParada.setDistanciaEnRuta(infoTramo.obtenerDistanciaEnRuta(puntoParada.getLatitud(), puntoParada.getLongitud()));
					if (paradasPorRuta.indexOf(puntoParada) == 0) {
						estimacionLlegada += puntoParada.getSegundosAnterior();
					} else {
						estimacionLlegada += puntoParada.getSegundosAnterior() * 1000 + (ValoresSimulacion.TIEMPO_DETENCION * 1000);
					}

					puntoParada.setTiempoEsperadoArribo(estimacionLlegada);
					break;
				}

			}

			i++;
		}
		return paradasPorRuta;
	}

	// getters y setters
	public int getVelocidadMH() {
		return velocidadMH;
	}

	public void setVelocidadMH(int velocidadMH) {
		this.velocidadMH = velocidadMH;
	}

	public List<LatLng> getRuta() {
		return ruta;
	}

	public void setRuta(List<LatLng> ruta) {
		this.ruta = ruta;
	}

	public boolean isTraficoPesado() {
		return traficoPesado;
	}

	public void setTraficoPesado(boolean traficoPesado) {
		this.traficoPesado = traficoPesado;
	}

	public Viaje getViaje() {
		return viaje;
	}

	public void setViaje(Viaje viaje) {
		this.viaje = viaje;
	}

	public boolean isStopThread() {
		return stopThread;
	}

	public void setStopThread(boolean stopThread) {
		this.stopThread = stopThread;
	}
}
